Amazon EC2のバックアップスクリプトを書いてみました。
こんにちは、城内です。 今クールのドラマが続々と最終回を向かえ、意外に面白かったセカムズも終わってしまいました。ムズムズが止まらないドラマでしたが、最終回はいい感じの展開でよかったです!
というわけで、今回は、最近運用スクリプトを書くことがあったので、一部をご紹介したいと思います。 (オリジナルは共通関数とかいろいろあるので、ちょっと簡素化しています)
はじめに
今回ご紹介するのは、EC2のバックアップスクリプトです。 中身はEC2インスタンスからAMIを作成するだけのもので、おそらくもう多くの方々が書かれたかと思いますが、意外に自分の要望にビシッとはまるものがなく、改めて書いてしまいました。
ちなみに、過去の記事で今回のようなスクリプトをまとめたものがありますので、ぜひこちらもご覧ください。
スクリプト紹介
では、さっそく、、、ドン! (※88行目のIFSにはタブを代入しています)
#!/bin/bash ## 変数定義 usage_message="[--backup-group <value>] [--no-reboot] [--region <value>]" return_code=0 _IFS=$IFS script_name="$(basename $0)" tmp_file="/tmp/$(basename $0 .sh).$$" backup_group_tag_key="BackupGroup" # バックアップグループタグのキー名 backup_session_id_tag_key="BackupSessionId" # バックアップセッションIDのキー名 backup_ami_name_prefix="Backup of " # バックアップで作成するAMI名の接頭辞 ## 関数定義 function usage { echo "Usage: $script_name $usage_message" exit $return_code } ## メイン処理 # 一時ファイルの削除 trap 'test -f $tmp_file && rm -f $tmp_file ; echo ""; exit 255' 1 2 3 15 # 引数の処理 backup_group= reboot_option= while [ $# -gt 0 ] do case "$1" in "--backup-group") if [ -n "$backup_group" ] then usage else if [ -z "$2" ] then usage else backup_group="$2" shift 2 fi fi ;; "--no-reboot") reboot_option="$1" shift ;; "--region") if [ -z "$2" ] then usage else export AWS_DEFAULT_REGION="$2" shift 2 fi ;; *) usage ;; esac done # スクリプト実行サーバのEC2インスタンスIDを取得 my_instance_id=$(curl -s http://169.254.169.254/latest/meta-data/instance-id 2> /dev/null) # EC2インスタンスの情報を一時ファイルに出力 aws ec2 describe-instances | jq '.Reservations[].Instances[] | select(.State.Name != "terminated")' > $tmp_file 2> /dev/null # EC2インスタンスが存在しない場合は終了 if [ ! -s $tmp_file ] then return_code=2 echo "Warning: EC2 instance does not exist." rm -f $tmp_file exit $return_code fi # 引数でバックアップグループの指定がない場合は対話形式 target_instances= if [ -z "$backup_group" ] then # EC2インスタンスの一覧表示 printf "\n %-5s %-30s %-20s %-15s %-15s %-15s %-20s\n" "No" "Instance Name" "Instance ID" "Public IP" "Private IP" "Instance State" "Backup Group" echo "----------------------------------------------------------------------------------------------------------------------------------" IFS=" " number=1 min_number=$number max_number=$number while read instance_name instance_id public_ip private_ip instance_state backup_group do if [ "$instance_name" = " " ] then array_instance_info[$number]="${instance_id},no name" else array_instance_info[$number]="${instance_id},${instance_name}" fi printf " %-5d %-30s %-20s %-15s %-15s %-15s %-20s\n" "$number" "$instance_name" "$instance_id" "$public_ip" "$private_ip" "$instance_state" "$backup_group" number=$((number+1)) done <<< "$(cat $tmp_file | jq -r '[if .Tags != null then (if (.Tags[] | select(.Key == "Name").Value) == "" then " " else (.Tags[] | select(.Key == "Name").Value) end) else " " end, .InstanceId, if .PublicIpAddress != null then .PublicIpAddress else " " end, .PrivateIpAddress, .State.Name, if .Tags != null then (.Tags[] | select(.Key == "'$backup_group_tag_key'").Value) else " " end] | join("\t")' | sort)" IFS=$_IFS max_number=$((number-1)) # EC2インスタンスの選択(ユーザ入力) echo "" number=0 read -p "Please select a instance number [${min_number}-${max_number}]> " number # 対象EC2インスタンスの設定 if [ $((number+1)) > /dev/null 2>&1 -a $number -ge $min_number -a $number -le $max_number ] then target_instances="${array_instance_info[$number]}" backup_group="manual" else return_code=1 echo "${script_name}: [ERROR] An invalid value was entered." exit $return_code fi # 再起動オプションの選択(デフォルトは再起動あり) # ただし、対象EC2インスタンスがスクリプトの実行インスタンスだった場合、再起動のオプションは強制で無効に設定 if [ "${target_instances%%,*}" = "$my_instance_id" ] then echo "Info: The '--no-reboot' option is on because my place is a target instance." reboot_option="--no-reboot" elif [ -z "$reboot_option" ] then read -p "Do you want to reboot when you backup the server? [Y|n]> " user_input case "$user_input" in [yY] | "") reboot_option="--reboot" ;; [nN]) reboot_option="--no-reboot" ;; *) return_code=1 echo "${script_name}: [ERROR] An invalid value was entered." exit $return_code ;; esac fi # 引数でバックアップグループが指定されている場合、バックアップ対象を抽出 else target_instances="$(cat $tmp_file | jq -r 'if .Tags != null then select(.Tags[].Value == "'$backup_group'") else empty end | .InstanceId + "," + if .Tags != null then (if (.Tags[] | select(.Key == "Name").Value) == "" then "no name" else (.Tags[] | select(.Key == "Name").Value) end) else "no name" end' 2> /dev/null)" # 対象のEC2インスタンスが存在しない場合は終了 if [ -z "$target_instances" ] then return_code=2 echo "Warning: Instance with a tag of \"Key:${backup_group_tag_key} Value:${backup_group}\" does not exist." rm -f $tmp_file exit $return_code fi fi backup_session_id="${backup_group}-$(date '+%Y%m%d%H%M%S')" echo -e "\nBackup Session ID: $backup_session_id" IFS=" " for target_instance in $target_instances do instance_id="${target_instance%%,*}" instance_name="${target_instance#*,}" # 対象EC2インスタンスがスクリプトの実行インスタンスだった場合、再起動のオプションは強制で無効に設定 no_reboot_flag=false if [ "$instance_id" = "$my_instance_id" ] then if [ -z "$reboot_option" ] then reboot_option="--no-reboot" no_reboot_flag=true fi fi # バックアップ処理(AMI作成とタグ付け)の実行 tags= echo -n "Start backing up $instance_id (${instance_name}) ... " ami_id=$(aws ec2 create-image --instance-id $instance_id --name ${instance_id}_$(date '+%Y%m%d%H%M%S') $reboot_option 2> $tmp_file | jq -r '.ImageId') if [ "${ami_id%%-*}" = "ami" ] then # バックアップ対象のEC2インスタンスからAMIに引き継ぐ情報(タグ付け) # ・キーペア # ・セキュリティグループ # ・インスタンスタイプ # ・サブネット # ・ロール tags="Key=\"Name\",Value=\"${backup_ami_name_prefix}${instance_id} (${instance_name})\" Key=\"${backup_session_id_tag_key}\",Value=\"${backup_session_id}\"" buff=$(aws ec2 describe-instances --instance-ids $instance_id | jq -c '.[][].Instances[] | {KeyName}, (.NetworkInterfaces[].Groups[] | {GroupId}), {InstanceType}, {SubnetId}, if .IamInstanceProfile.Arn != null then {"IamInstanceProfile": .IamInstanceProfile.Arn | sub(".*/"; "")} else empty end' | sed -e 's/^{//' -e 's/}$//' -e 's/,/\t/g') prev_tag_key= for tag in $buff do tag_key="${tag%:*}" tag_value="${tag#*:}" if [ "$prev_tag_key" = "$tag_key" ] then tags="${tags%\"} ${tag_value#\"}" else tags="$tags Key=$tag_key,Value=$tag_value" fi prev_tag_key="$tag_key" done # 作成したAMIにタグを付ける command="aws ec2 create-tags --resources $ami_id --tags $tags" eval $command 2> $tmp_file 1> /dev/null if [ $? -eq 0 ] then echo "done" else echo "failed" cat $tmp_file return_code=1 fi else echo "failed" cat $tmp_file return_code=1 fi if $no_reboot_flag then echo "Warning: Reboot did not work because my place is a target instance." reboot_option= fi done IFS=$_IFS test -f $tmp_file && rm -f $tmp_file exit $return_code
解説
まず前提として、実行する際は、EC2とIAMの一部(ロール周り)の権限を持っている必要があります。
処理の流れとしては、バックアップ対象のEC2インスタンスを決めて、そこからAMIを作成します。そして、バックアップ元のEC2インスタンスからリストアに必要な一部の情報を取得し、AMIにタグとして情報を付与しています。
今回のスクリプトは、運用で使用することを念頭において作成しています。ポイントは以下の3つです。
- 対話形式で対象のEC2インスタンスを選択できる
- リストア時のオペレーションを簡素化できる
- 複数のEC2インスタンスをグルーピングしてバックアップすることができる
1つ目は、以下のような挙動になります。
$ ./ec2_backup_instance.sh No Instance Name Instance ID Public IP Private IP Instance State Backup Group ---------------------------------------------------------------------------------------------------------------------------------- 1 Web Server i-xxxxxxxx xx.xx.xx.xx xx.xx.xx.xx running xx-system-01 2 App Server #01 i-yyyyyyyy yy.yy.yy.yy running xx-system-02 3 App Server #02 i-zzzzzzzz zz.zz.zz.zz running xx-system-02 ... Please select a instance number [1-n]>
オペレータが作業しやすいように考えてみました。
2つ目は、バックアップしたAMIからのリストアするためには、新たなEC2インスタンスを作成することになるため、最低限必要ないくつかのパラメータを指定する必要がありました。 そこに柔軟性があることはメリットではありますが、オペレータが単純に同じ設定のEC2インスタンスをリストアしようとした場合は、元の情報を揃える必要があるという手間が発生してしまうため、そこをうめるような仕組みを実装してみました。
3つ目は、オプションの--backup-groupを利用することで、BackupGroupタグを付けた複数のEC2インスタンスをまとめてバックアップすることができます。
$ ./ec2_backup_instance.sh --backup-group "xx-system-02" Backup Session ID: xx-system-02-20160601000000 Start backing up i-yyyyyyyy (App Server #01) ... done Start backing up i-zzzzzzzz (App Server #02) ... done
すでにバックアップ対象に専用タグを付けるという方式はあったのですが、その方式の場合、EC2インスタンス毎のバックアップタイミングをコントロールしづらいという悩みがあったので、タグの値を使ってグループ分けができるようにしてみました。
こちらは、ジョブで実行するケースを想定しています。
さいごに
今回は、実際の運用で使えるようなスクリプトをご紹介しました。
こちらのスクリプトはそのまま使えるはずですが、もしかしたら細かいところでおやっとなるかもしれません。 その辺り、オリジナルは前処理やprintfの拡張関数を組み込んであり、もう少し手間をかけてあります。 また、今回のスクリプトを実行した後に、AMIの一覧を表示したくなったり、リストアのスクリプトも必要じゃん、となるかと思います。
もし今回ご紹介したようなAWSの運用スクリプトをご要望でしたら、ぜひ弊社メンバーズサービスをご利用くださいませ!(強引な宣伝w RDSやRedshiftのバックアップ・リストアスクリプトもありますよー!